package com.zhuhjay.service.system.service.impl;

import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.benmanes.caffeine.cache.Cache;
import com.zhuhjay.common.base.exception.BasicBusinessException;
import com.zhuhjay.common.base.model.domain.MailTemplate;
import com.zhuhjay.common.base.util.JacksonUtils;
import com.zhuhjay.common.base.util.MailSenderUtils;
import com.zhuhjay.common.core.util.SecurityContextHolder;
import com.zhuhjay.common.redis.util.RedisUtils;
import com.zhuhjay.common.security.context.LoginUserContext;
import com.zhuhjay.common.web.model.vo.PageInfoVo;
import com.zhuhjay.service.system.enums.OauthTypeEnum;
import com.zhuhjay.service.system.event.UserOfflineEvent;
import com.zhuhjay.service.system.mapper.SystemUserMapper;
import com.zhuhjay.service.system.model.domain.EmailRedisPayload;
import com.zhuhjay.service.system.model.dto.UserPageParamDto;
import com.zhuhjay.service.system.model.dto.UserRegisterDto;
import com.zhuhjay.service.system.model.entity.SystemRole;
import com.zhuhjay.service.system.model.entity.SystemUser;
import com.zhuhjay.service.system.model.entity.SystemUserRole;
import com.zhuhjay.service.system.model.vo.UserInfoVo;
import com.zhuhjay.service.system.service.SystemConfigService;
import com.zhuhjay.service.system.service.SystemRoleService;
import com.zhuhjay.service.system.service.SystemUserRoleService;
import com.zhuhjay.service.system.service.SystemUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.Date;
import java.util.stream.Collectors;

import static com.zhuhjay.common.base.constant.CacheConstant.*;
import static com.zhuhjay.common.base.constant.TimeConstant.MINUTE;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author Zhuhjay
 * @since 2023-01-11
 */
@Slf4j
@Service
public class SystemUserServiceImpl extends ServiceImpl<SystemUserMapper, SystemUser>
        implements SystemUserService {

    @Resource
    private PasswordEncoder passwordEncoder;
    @Resource
    private SystemRoleService roleService;
    @Resource
    private SystemUserRoleService userRoleService;
    @Resource
    private ApplicationEventPublisher publisher;
    @Resource
    private SystemConfigService configService;
    @Resource
    private Cache<Integer, Object> userInfoCache;

    @Override
    @Transactional(rollbackFor = {BasicBusinessException.class, Exception.class})
    public void register(UserRegisterDto userRegister) {
        // 用户名不能重复
        Long count = this.lambdaQuery()
                .eq(SystemUser::getUsername, userRegister.getUsername()).count();
        if (count == null || count != 0) {
            throw new BasicBusinessException("该用户名已注册");
        }
        // 进行密码加密
        userRegister.setPassword(passwordEncoder.encode(userRegister.getPassword()));
        SystemUser user = userRegister.bean2Entity();
        // 让此时的昵称与用户名相同
        user.setNickName(userRegister.getUsername());
        if (!this.save(user)) {
            throw new BasicBusinessException("注册失败，请重试");
        }
        // 将用户的角色设置为普通用户
        if (!roleService.bindToUser(user.getId())) {
            throw new BasicBusinessException("注册失败，请重试");
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateStatus(Integer userId) {
        SystemUser user = this.lambdaQuery().eq(SystemUser::getId, userId).one();
        if (user == null) {
            throw new BasicBusinessException("用户不存在");
        }
        // 更新用户状态
        user.setStatus(user.getStatus() == 1 ? 0 : 1);
        this.updateById(user);
        // 将该用户所有在线状态强制退出
        if (user.getStatus() == 0) {
            // 触发下线事件(移除所有连接)
            publisher.publishEvent(new UserOfflineEvent(this, user.getUsername(), null));
        }
    }

    @Override
    public PageInfoVo<UserInfoVo> usersPage(Integer pageNum, Integer pageSize, UserPageParamDto param) {
        // 分页查询
        Page<SystemUser> userPage = this.lambdaQuery()
                .eq(param.getStatus() != null, SystemUser::getStatus, param.getStatus())
                .like(StrUtil.isNotBlank(param.getUsername()), SystemUser::getUsername, param.getUsername())
                .ge(param.getStartTime() != null, SystemUser::getCreateTime, param.getStartTime())
                .le(param.getEndTime() != null, SystemUser::getCreateTime, param.getEndTime())
                .page(Page.of(pageNum, pageSize));
        // 将查询结果转换为UserInfoVo
        PageInfoVo<UserInfoVo> result = new PageInfoVo<>();
        result.setCurrent(userPage.getCurrent());
        result.setSize(userPage.getSize());
        result.setTotal(userPage.getTotal());
        // 是否需要脱敏
        final boolean isDesensitization = !SecurityContextHolder.isAdmin();
        result.setRecords(userPage.getRecords().stream().map(user -> {
            UserInfoVo userInfoVo = UserInfoVo.of(user, isDesensitization);
            // 设置角色
            userInfoVo.setRole(roleService.getRoleByUserId(user.getId()));
            return userInfoVo;
        }).collect(Collectors.toList()));
        return result;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateRole(Integer userId, Integer roleId) {
        // 查询用户是否存在
        if (this.lambdaQuery().eq(SystemUser::getId, userId).one() == null) {
            throw new BasicBusinessException("用户不存在");
        }
        // 查询角色是否存在
        if (roleService.lambdaQuery().eq(SystemRole::getId, roleId).one() == null) {
            throw new BasicBusinessException("角色不存在");
        }
        // 判断修改的角色是否与用户角色相同
        if (userRoleService.lambdaQuery()
                .eq(SystemUserRole::getUserId, userId)
                .eq(SystemUserRole::getRoleId, roleId).one() != null) {
            throw new BasicBusinessException("用户角色未发生变化");
        }
        // 更新用户角色
        userRoleService.lambdaUpdate()
                .set(SystemUserRole::getRoleId, roleId)
                .eq(SystemUserRole::getUserId, userId).update();
    }

    @Override
    public void checkPassword(String password) {
        Integer userId = LoginUserContext.getUserId();
        SystemUser user = this.lambdaQuery().eq(SystemUser::getId, userId).one();
        if (user == null) {
            throw new BasicBusinessException("密码修改失败，请重试");
        }
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new BasicBusinessException("原密码错误");
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updatePassword(String password) {
        Integer userId = LoginUserContext.getUserId();
        this.lambdaUpdate().eq(SystemUser::getId, userId)
                .set(SystemUser::getPassword, passwordEncoder.encode(password)).update();
    }

    @Override
    public void send2BindEmail(String email) {
        // 判断用户是否已经绑定过邮箱
        Integer userId = LoginUserContext.getUserId();
        SystemUser user = this.lambdaQuery().eq(SystemUser::getId, userId).one();
        if (StrUtil.isNotBlank(user.getEmail())) {
            throw new BasicBusinessException("您已绑定邮箱");
        }
        // 判断邮箱是否被绑定过
        if (this.lambdaQuery().eq(SystemUser::getEmail, email).one() != null) {
            throw new BasicBusinessException("该邮箱已被绑定");
        }
        sendEmail(userId, BIND_EMAIL_PREFIX,
                MailTemplate.of(
                        email, MailSenderUtils.MailSubject.BIND_EMAIL,
                        getFrontUrl(), "/verify/bind_email/{email}/{code}"));
    }

    @Override
    public void send2UnbindEmail() {
        // 判断是否绑定邮箱
        Integer userId = LoginUserContext.getUserId();
        SystemUser user = this.lambdaQuery().eq(SystemUser::getId, userId).one();
        String email = user.getEmail();
        if (StrUtil.isBlank(email)) {
            throw new BasicBusinessException("未绑定邮箱");
        }
        sendEmail(userId, UNBIND_EMAIL_PREFIX,
                MailTemplate.of(
                        email, MailSenderUtils.MailSubject.UNBIND_EMAIL,
                        getFrontUrl(), "/verify/unbind_email/{email}/{code}"));
    }

    @Override
    public void send2ResetPwd(SystemUser user) {
        sendEmail(user.getId(), RESET_PASSWORD_PREFIX,
                MailTemplate.of(
                        user.getEmail(), MailSenderUtils.MailSubject.RESET_PASSWORD,
                        getFrontUrl(), "/forget/{email}/{code}"));
    }

    /** 获取前端地址 */
    private String getFrontUrl() {
        String url = configService.getConfigValue("system.front.base.url");
        if (!Validator.isUrl(url)) {
            log.error("系统配置错误, 请检查${{}}配置项", "system.front.base.url");
            throw new BasicBusinessException("请求失败, 请联系管理员");
        }
        return url;
    }

    /**
     * 发送邮件信息
     */
    private static void sendEmail(Integer userId, String cachePredix, MailTemplate mailTemplate) {
        String key = cachePredix + mailTemplate.getTo();
        // 查看是否已经发送过授权码
        String cache = RedisUtils.getCache(key);
        if (StrUtil.isNotBlank(cache)) {
            throw new BasicBusinessException("已发送，请查看邮箱");
        }
        // 生成授权码
        String code = IdUtil.fastSimpleUUID();
        // 解析 href 参数, 将授权码放入 href 中
        mailTemplate.parseHref(code);
        EmailRedisPayload payload = EmailRedisPayload.of(userId, code);
        MailSenderUtils.sendHtml(mailTemplate);
        RedisUtils.setCacheAndExpire(key, payload, 10 * MINUTE);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void resetPasswordByAdmin(Integer userId, String password) {
        this.lambdaUpdate()
                .eq(SystemUser::getId, userId)
                .set(SystemUser::getPassword, passwordEncoder.encode(password)).update();
    }

    @Override
    public void updateLastLoginTime(String username) {
        this.lambdaUpdate().eq(SystemUser::getUsername, username)
                .set(SystemUser::getLastLoginTime, new Date()).update();
    }

    @Override
    public String getAvatar(String username) {
        return this.lambdaQuery().eq(SystemUser::getUsername, username)
                .oneOpt().map(SystemUser::getAvatar).orElse(null);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public SystemUser createAccount(OauthTypeEnum oauthTypeEnum, String openid) {
        // 生成用户名(最长10位)
        String username = oauthTypeEnum.getType() + '_' +
                RandomUtil.randomString(1) +
                StrUtil.subWithLength(openid, 0, 3) +
                StrUtil.subWithLength(openid, -1, 3);
        // 生成密码
        String password = RandomUtil.randomStringWithoutStr(8, "0oO1iIlL");
        SystemUser user = new SystemUser();
        user.setUsername(username);
        user.setPassword(passwordEncoder.encode(password));
        user.setNickName(username);
        this.save(user);
        // 绑定角色
        roleService.bindToUser(user.getId());
        user.setPassword(password);
        return user;
    }

    @Override
    public void bindEmail(String email, String code) {
        String key = BIND_EMAIL_PREFIX + email;
        EmailRedisPayload payload = checkAndGetPayload(code, key);
        // 再次判断邮箱是否被绑定过
        if (this.lambdaQuery().eq(SystemUser::getEmail, email).one() != null) {
            throw new BasicBusinessException("该邮箱已被绑定");
        }
        // 绑定邮箱
        this.lambdaUpdate().eq(SystemUser::getId, payload.getUserId())
                .set(SystemUser::getEmail, email).update();
        RedisUtils.deleteCache(key);
    }

    @Override
    public void unbindEmail(String email, String code) {
        String key = UNBIND_EMAIL_PREFIX + email;
        EmailRedisPayload payload = checkAndGetPayload(code, key);
        // 解绑邮箱
        this.lambdaUpdate().eq(SystemUser::getId, payload.getUserId())
                .set(SystemUser::getEmail, null).update();
        RedisUtils.deleteCache(key);
    }

    @Override
    public void resetPasswordByEmail(String email, String code, String password) {
        String key = RESET_PASSWORD_PREFIX + email;
        EmailRedisPayload payload = checkAndGetPayload(code, key);
        // 重置密码
        this.lambdaUpdate()
                .eq(SystemUser::getId, payload.getUserId())
                .set(SystemUser::getPassword, passwordEncoder.encode(password)).update();
        RedisUtils.deleteCache(key);
    }

    @Override
    public String getCreateUserByCaffeine(Integer userId) {
        if (userId == null) {
            return null;
        }
        return ((SystemUser) userInfoCache.get(userId, this::getById)).getUsername();
    }

    /**
     * 校验并获取邮箱验证码载荷
     */
    private EmailRedisPayload checkAndGetPayload(String code, String key) {
        String cache = RedisUtils.getCache(key);
        if (StrUtil.isBlank(cache)) {
            throw new BasicBusinessException("请求失败, 请重试");
        }
        EmailRedisPayload payload = JacksonUtils.readValue(cache, EmailRedisPayload.class);
        if (!code.equals(payload.getCode())) {
            throw new BasicBusinessException("非法请求");
        }
        return payload;
    }

}
